import os.path
from pyfileutils import read_file, write_file

try:
    from xml.etree import cElementTree as ET
except ImportError:
    try:
        from xml.etree import ElementTree as ET
    except ImportError:
        try:
            import cElementTree as ET
        except ImportError:
            try:
                from elementtree import ElementTree as ET
            except ImportError:
                raise Exception("Could not import ElementTree")

class EntryNotFoundError(Exception):

    def __init__(self, label, id):
        """Raises an EntryNotFoundError."""
        Exception.__init__(self, '%s "%s" not found' % (label.capitalize(), id))

class EntryExistsError(Exception):

    def __init__(self, label, id):
        """Raises an EntryExistsError."""
        es = '%s "%s" already exists' % (label.capitalize(), id)
        Exception.__init__(self, es)

class MultipleEntriesFoundError(Exception):

    def __init__(self, label, id):
        """Raises a MultipleEntriesFoundError."""
        es = 'More than one %s has id "%s"' % (label.capitalize(), id)
        Exception.__init__(self, es)

class UnsupportedAttributeError(Exception):

    def __init__(self, attribute):
        """Raises an UnsupportedAttributeError"""
        Exception.__init__(self, "Attribute %s is unsupported" % (attribute))

class SchemaException(Exception): pass

class Schema:

    """Schema is an base schema class for CDB.
    
    Schema have these requirements:
    
    1. Entries must have a 'id' attribute.
    2. The 'id' attribute's value must not evaluate to False or None.
    3. Every entry's 'id' attribute must be unique in the schema.

    Functionally, it's like saying that all entries must have a
    (NOT NULL UNIQUE PRIMARY KEY) column.
    
    """

    schema_label = u'schema'        # Subclasses can change this.
    base_file_name = 'schema.xml'   # Subclasses can change this.
    schema_entry_label = u'entry'   # Subclasses can change this.
    supported_attributes = ['id']   # Subclasses can change this.

    def __init__(self, folder_name):
        """Initializes a Schema.

        folder_name: a string representing the folder which will
                     contain the schema.

        """
        if not os.path.isdir(folder_name):
            es = "Could not find folder %s"
            raise SchemaException, es % (folder_name)
        if folder_name.endswith('/') or folder_name.endswith('\\'):
            folder_name = folder_name[:-1]
        self.folder_name = folder_name
        self.name = os.path.basename(folder_name)
        self.filename = os.path.abspath(os.path.join(folder_name,
                                                     self.base_file_name))
        if os.path.isfile(self.filename):
            self._read_schema_from_file()
        else:
            self._create_new_schema()

    def _read_schema_from_file(self):
        """Reads a schema from a file if it exists."""
        schema_data = read_file(self.filename)
        self._el = ET.fromstring(schema_data)

    def _create_new_schema(self):
        """Creates a new schema if one does not exist."""
        self._el = ET.Element(self.schema_label)
        self._el.attrib['name'] = self.name
        self._save_schema_to_disk()

    def _save_schema_to_disk(self):
        """Writes the schema in memory to disk, syncing the two."""
        schema_data = ET.tostring(self._el)
        write_file(schema_data, self.filename, overwrite=True)

    def _get_entry(self, entry_id):
        """Returns an entry.

        entry_id: a string representing the id of the entry to be
                  returned.

        """
        entries = self._el.getchildren()
        entries = [entry for entry in entries if entry.attrib['id'] == entry_id]
        number_of_entries = len(entries)
        if number_of_entries < 1:
            raise EntryNotFoundError(self.schema_entry_label, entry_id)
        elif number_of_entries > 1:
            raise MultipleEntriesFoundError(self.schema_entry_label, entry_id)
        return entries[0]

    def _get_entries(self):
        """Returns a list of entries."""
        return self._el.getchildren()

    def _list_attribute(self, entry_id, attribute):
        """Lists the value of 'attribute' for every entry.

        attribute: a string representing the name of the attribute
                   whose value is to be listed.

        """
        if attribute not in self.supported_attributes:
            raise UnsupportedAttributeError(attribute)
        return self._get_entry(entry_id).attrib[attribute]

    def _list_attributes(self, attribute):
        """Lists the value of 'attribute' for every entry.

        attribute: a string representing the name of the attribute
                   whose value is to be listed.

        For example, say our schema looks like this:

        <?xml version="1.0" encoding="UTF-8">
        <schema>
            <entry id="BlogEntries" />
            <entry id="BlogUsers" />
        </schema>

        >>> schema._list_attribute('id')
        ['BlogEntries', 'BlogUsers']

        'attribute' must be in self.supported_attributes.

        """
        entries = self._get_entries()
        x = [self._list_attribute(e.attrib['id'], attribute) for e in entries]
        return x

    def _set_attribute(self, entry_id, attribute, value):
        """Sets an attribute's value for a specified entry.

        entry_id:   a string representing the id of the entry to
                    modify.
        attribute:  a string representing the name of the attribute to
                    modify.
        value:      a string representing the new value of the
                    attribute.

        'attribute' must be in self.supported_attributes

        """
        if attribute not in self.supported_attributes:
            raise UnsupportedAttributeError(attribute)
        if attribute == 'id' and value in self._list_entry_ids():
            raise EntryExistsError(self.schema_entry_label, value)
        self._get_entry(entry_id).attrib[attribute] = value
        self._save_schema_to_disk()

    def _set_attributes(self, attribute, value):
        """Set an attribute's  value for all entries.

        attribute:  a string representing the attribute to modify.
        value:      a string representing the new value of the
                    attribute.

        'attribute' must be in self.supported_attributes

        """
        for entry in self._get_entries():
            self._set_attribute(entry.attrib['id'], attribute, value)

    def _list_entry_ids(self):
        """Returns a list of all entry ids."""
        return self._list_attributes('id')

    def _add(self, values):
        """Adds an entry to this schema.

        values:  a sequence containing values for each attribute in
                 self.supported_attributes.  These sequences must map
                 directly.

        """
        entry_id = values[0]
        if entry_id in self._list_entry_ids():
            raise EntryExistsError(self.schema_entry_label, entry_id)
        number_of_values = len(values)
        number_of_attributes = len(self.supported_attributes)
        if number_of_values < number_of_attributes:
            raise ValueError, "Not enough values given for attributes"
        elif number_of_values > number_of_attributes:
            raise ValueError, "Too many values given for attributes"
        entry_el = ET.SubElement(self._el, self.schema_entry_label)
        entry_el.attrib['id'] = entry_id
        self._save_schema_to_disk()
        for attribute, value in zip(self.supported_attributes, values):
            if not attribute == 'id':
                self._set_attribute(entry_id, attribute, value)

    def _drop(self, entry_id):
        """Deletes an entry from this table.

        entry_id: a string representing the id of the entry.

        """
        self._el.remove(self._get_entry(entry_id))
        self._save_schema_to_disk()

    def _rename(self, entry_id, new_id):
        """Sets an entry's id.

        entry_id:    a string representing the id of the entry.
        new_id:      a string representing the new id of the entry.

        """
        self._set_attribute(entry_id, 'id', new_id)

